classdef EnsembleClassifierChains ...
        < MultiLabelAlgorithm ...
        & RandomSeedable ...
        & MultiLabelAlgorithm0able ...
        & ZAlgorithmable ...
        & Modelable ...
        & DecisionThresholdable ...
        & MultiLabelMetricable
    %ENSEMBLECLASSIFIERCHAINS Summary of this class goes here
    %   Detailed explanation goes here
    
    properties ( SetAccess = protected )
        nEnsemble = 10
        rEnsemble % Reserved
        zLabelSequence = {}
        thresholdMode = 'fixed'
    end
    
    methods % Set methods
        function [  ] = setNEnsemble( this, nEnsemble )
            %SETNENSEMBLE Summary of this function goes here
            %   Detailed explanation goes here
            % EnsembleClassifierChains::
            
            if nargin == 1
                this.nEnsemble = 10;
                return
            end
            if nargin == 2
                this.nEnsemble = nEnsemble;
                return
            end
        end
        
        function [  ] = setREnsemble( this, rEnsemble )
            %SETRENSEMBLE Summary of this function goes here
            %   Detailed explanation goes here
            % EnsembleClassifierChains::
            
            if nargin == 1
                this.rEnsemble = [];
                return
            end
            if nargin == 2
                this.rEnsemble = rEnsemble;
                return
            end
        end
        
        function [  ] = setZLabelSequence( this, zLabelSequence )
            %SETZLABELSEQUENCE Summary of this function goes here
            %   Detailed explanation goes here
            this.zLabelSequence = zLabelSequence;
        end
        
        function [  ] = setThresholdMode(this, thresholdMode)
            this.thresholdMode = thresholdMode;
        end
    end
    
    methods
        function [ this ] = EnsembleClassifierChains( name, nEnsemble )
            if nargin == 0
                this.setName('ecc');
            end
            if nargin >= 1
                if isnan(name)
                    name = 'ecc';
                end
                this.setName(name);
            end
            if nargin >= 2
                if isnan(nEnsemble)
                    nEnsemble = 10;
                end
                this.setNEnsemble(nEnsemble);
            end
            algorithm0 = ClassifierChains();
            this.setAlgorithm0(algorithm0);
        end
    end
    
    methods
        function [  ] = build( this, X1, Y1 )
            %BUILD Summary of this function goes here
            %   Detailed explanation goes here
            
            nLabel = size(Y1, 2);
            
            this.shuffleLabelSequence(nLabel); % Set `this.zLabelSequence`
            this.model.labelCardinality = nnz(Y1 == 1)/size(Y1, 1);
            
            this.zAlgorithm = cell(1, this.nEnsemble);
            for iEnsemble = 1:this.nEnsemble
                Y1_i = Y1(:, this.zLabelSequence{iEnsemble});
                this.zAlgorithm{iEnsemble} = this.algorithm0.clone();
                this.zAlgorithm{iEnsemble}.build(X1, Y1_i);
            end
        end
        
        function [ result ] = apply( this, X2, Y2 )
            %APPLY Summary of this function goes here
            %   Detailed explanation goes here
            % EnsembleClassifierChains
            
            Y2_hat = zeros(size(Y2));
            Y2_out = Y2_hat;
            for iEnsemble = 1:this.nEnsemble
                Y2_i = Y2(:, this.zLabelSequence{iEnsemble});
                result2_i = this.zAlgorithm{iEnsemble}.apply(X2, Y2_i);
                [~, zRecoverId_i] = sort(this.zLabelSequence{iEnsemble}, 'ascend');
                Y2_hat = Y2_hat + result2_i.Y_hat(:, zRecoverId_i);
                Y2_out = Y2_out + result2_i.Y_out(:, zRecoverId_i);
            end
            
            Y2_out = Y2_hat/this.nEnsemble;
            Y2_hat = Y2_hat/this.nEnsemble;
                        
            nObs = size(Y2, 1);
            if strcmp(this.thresholdMode, 'auto')
                thresholdCandidate = -1:0.05:1;
                for i_thresholdCandidate = 1:length(thresholdCandidate)
                    fvalue(i_thresholdCandidate) = ...
                        abs(nnz(Y2_hat >= thresholdCandidate(i_thresholdCandidate))/nObs ...
                        - this.model.labelCardinality);
                end
                [~, minId] = min(fvalue);
                threshold = thresholdCandidate(minId);
            else
                threshold = this.decisionThreshold;
            end
            
            
            Y2_hat(Y2_hat > threshold) = 1;
            Y2_hat(Y2_hat ~= 1) = -1;
            
            result.Y_hat = Y2_hat;
            result.Y_out = Y2_out;
        end
    end
    
    methods
        function [  ] = shuffleLabelSequence( this, nLabel )
            %GENZLABELSEQUENCE Summary of this function goes here
            %   Detailed explanation goes here
            % EnsembleClassifierChains::
            
            this.zLabelSequence = cell(1, this.nEnsemble);
            
            rng(this.randomSeed);
            for iEnsemble = 1:this.nEnsemble
                this.zLabelSequence{iEnsemble} = randperm(nLabel);
            end
        end
    end
    
end

